﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Xml;
using FA.Library.Plugin;
using FA.FlightAttendant.Properties;
using NLog;

namespace FA.FlightAttendant.Plugin
{
    public class PluginFactory : IPluginFactory
    {
        private static readonly Logger _logger = LogManager.GetCurrentClassLogger();

        #region Singleton

        /// <summary>
        /// Singleton instance.
        /// </summary>
        public static PluginFactory Instance
        {
            get { return _instance; }
        }
        private static readonly PluginFactory _instance = new PluginFactory();

        #endregion

        #region Properties

        public ObservableCollection<PluginLibrary> PluginLibraries
        {
            get
            {
                return _pluginLibraries;
            }
        }

        #endregion

        private ObservableCollection<PluginLibrary> _pluginLibraries;

        /// <summary>
        /// Private constructor.
        /// </summary>
        private PluginFactory()
        {            
            _pluginLibraries = new ObservableCollection<PluginLibrary>();
        }

        /// <summary>
        /// Load the plugins.
        /// </summary>
        public void LoadPluginLibraries()
        {
            // Get the plugin directory
            DirectoryInfo dir = new DirectoryInfo(Path.Combine(Environment.CurrentDirectory, Settings.Default.PluginsFolder));
            if (!dir.Exists)
                dir.Create();

            foreach (FileInfo file in dir.GetFiles("*.dll", SearchOption.TopDirectoryOnly))
            {
                loadPluginLibrary(file.FullName);
            }
        }

        /// <summary>
        /// Create a plugin instance of the given class name (including namespace).
        /// </summary>
        /// <returns></returns>
        public IPlugin CreatePlugin(string classname)
        {
            // Create the instance
            IPlugin plugin = null;
            foreach (PluginLibrary pluginLib in _pluginLibraries)
            {
                if (pluginLib.Classname.Equals(classname))
                {
                    plugin = (IPlugin)pluginLib.Assembly.CreateInstance(classname);
                    break;
                }
            }
            if (plugin != null)
                return plugin;
            else
                throw new Exception("Could not find plugin '" + classname + "'.");
        }

        /// <summary>
        /// Get a plugin library object with the given classname.
        /// </summary>
        /// <param name="classname"></param>
        /// <returns></returns>
        public PluginLibrary GetPluginLibrary(string classname)
        {
            PluginLibrary plugin = null;

            foreach (PluginLibrary pluginLib in _pluginLibraries)
            {
                if (pluginLib.Classname.Equals(classname))
                {
                    plugin = pluginLib;
                    break;
                }
            }

            return plugin;
        }

        /// <summary>
        /// Load a plugin library from the provided path.
        /// </summary>
        /// <param name="pluginPath"></param>
        private void loadPluginLibrary(string pluginPath)
        {
            Assembly assembly = Assembly.LoadFile(pluginPath);
            PluginLibrary plugin = null;

            #region Locate the plugin meta-file

            string[] resourceNames = assembly.GetManifestResourceNames();
            foreach (string resource in resourceNames)
            {
                using (Stream s = assembly.GetManifestResourceStream(resource))
                {
                    try
                    {
                        using (XmlReader xmlReader = XmlReader.Create(s))
                        {
                            if (xmlReader.ReadToDescendant("plugin") && xmlReader.NodeType == XmlNodeType.Element)
                            {
                                try
                                {
                                    plugin = readPluginLibraryXML(xmlReader, assembly);
                                    break;
                                }
                                catch (Exception ex)
                                {
                                    _logger.InfoException("Unable to read XML in resource " + resource
                                        + " in plugin library '" + pluginPath + "'.", ex);
                                    continue;
                                }
                            }
                        }
                    }
                    catch (Exception ex)
                    {
                        _logger.InfoException("Unable to read resource " + resource + " as XML "
                            + " in plugin library '" + pluginPath + "'.", ex);
                        continue;
                    }
                }
                _logger.Info("Couldn't find 'plugin' XML element in resource " + resource
                    + " in plugin library '" + pluginPath + "'.");
            }

            if (plugin == null)
            {
                _logger.Warn("Failed to load plugin library '" + pluginPath + "'.");
                return;
            }

            #endregion

            // Test that the classname exists
            try
            {
                plugin.Assembly.GetType(plugin.Classname, true, false);
            }
            catch (Exception ex)
            {
                _logger.WarnException("Could not locate type " + plugin.Classname
                    + " in plugin library '" + pluginPath + "'.", ex);
                return;
            }

            // Check that there isn't already a plugin with this classname
            if (_pluginLibraries.Any((x) => (x.Classname.Equals(plugin.Classname))))
            {
                _logger.Warn("A plugin is already loaded with the classname '" + plugin.Classname + "'.");
                return;
            }

            _pluginLibraries.Add(plugin);
            _logger.Info("Loaded plugin '" + plugin.Classname + "'.");
        }

        /// <summary>
        /// Read the plugin details from within a <plugin></plugin> tag.
        /// </summary>
        /// <param name="reader"></param>
        /// <param name="assembly"></param>
        /// <returns></returns>
        private PluginLibrary readPluginLibraryXML(XmlReader reader, Assembly assembly)
        {
            string name = String.Empty;
            string classname = String.Empty;
            string author = String.Empty;
            string version = String.Empty;
            List<string> availableSettings = new List<string>();

            #region Read the XML elements

            while (reader.Read())
            {
                if (reader.NodeType == XmlNodeType.Element)
                {
                    if (reader.Name.Equals("name", StringComparison.OrdinalIgnoreCase))
                    {
                        name = reader.ReadElementContentAsString().Trim();
                    }
                    else if (reader.Name.Equals("pluginclass", StringComparison.OrdinalIgnoreCase))
                    {
                        classname = reader.ReadElementContentAsString().Trim();
                    }
                    else if (reader.Name.Equals("author", StringComparison.OrdinalIgnoreCase))
                    {
                        author = reader.ReadElementContentAsString().Trim();
                    }
                    else if (reader.Name.Equals("version", StringComparison.OrdinalIgnoreCase))
                    {
                        version = reader.ReadElementContentAsString().Trim();
                    }
                    else if (reader.Name.Equals("setting", StringComparison.OrdinalIgnoreCase))
                    {
                        availableSettings.Add(reader.ReadElementContentAsString().Trim());
                    }
                }
            }

            #endregion

            // Create the plugin library object
            if (!String.IsNullOrEmpty(name) && !String.IsNullOrEmpty(classname) && !String.IsNullOrEmpty(version))
            {
                return new PluginLibrary(name, classname, author, version, availableSettings.ToArray(), assembly);
            }
            else
            {
                throw new Exception("Plugin XML is missing 'name', 'classname' or 'version' element(s).");
            }
        }
    }
}